package bolt

import (
	"context"
	"encoding/json"
	"time"

	bolt "github.com/coreos/bbolt"
	platform "github.com/influxdata/influxdb"
)

var (
	sessionBucket = []byte("sessionsv1")
)

var _ platform.SessionService = (*Client)(nil)

func (c *Client) initializeSessions(ctx context.Context, tx *bolt.Tx) error {
	if _, err := tx.CreateBucketIfNotExists([]byte(sessionBucket)); err != nil {
		return err
	}
	return nil
}

// RenewSession extends the expire time to newExpiration.
func (c *Client) RenewSession(ctx context.Context, session *platform.Session, newExpiration time.Time) error {
	op := getOp(platform.OpRenewSession)
	if session == nil {
		return &platform.Error{
			Op:  op,
			Msg: "session is nil",
		}
	}
	return c.db.Update(func(tx *bolt.Tx) error {
		session.ExpiresAt = newExpiration
		if err := c.putSession(ctx, tx, session); err != nil {
			return &platform.Error{
				Op:  op,
				Err: err,
			}
		}
		return nil
	})
}

// FindSession retrieves the session found at the provided key.
func (c *Client) FindSession(ctx context.Context, key string) (*platform.Session, error) {
	op := getOp(platform.OpFindSession)
	var sess *platform.Session
	err := c.db.View(func(tx *bolt.Tx) error {
		s, err := c.findSession(ctx, tx, key)
		if err != nil {
			return err
		}

		sess = s
		return nil
	})

	if err != nil {
		return nil, &platform.Error{
			Err: err,
			Op:  op,
		}
	}

	if err := sess.Expired(); err != nil {
		// todo(leodido) > do we want to return session also if expired?
		return sess, &platform.Error{
			Err: err,
			Op:  op,
		}
	}
	return sess, nil
}

func (c *Client) findSession(ctx context.Context, tx *bolt.Tx, key string) (*platform.Session, *platform.Error) {
	v := tx.Bucket(sessionBucket).Get([]byte(key))
	if len(v) == 0 {
		return nil, &platform.Error{
			Code: platform.ENotFound,
			Msg:  platform.ErrSessionNotFound,
		}
	}

	s := &platform.Session{}
	if err := json.Unmarshal(v, s); err != nil {
		return nil, &platform.Error{
			Err: err,
		}
	}

	// TODO(desa): these values should be cached so it's not so expensive to lookup each time.
	f := platform.UserResourceMappingFilter{UserID: s.UserID}
	mappings, err := c.findUserResourceMappings(ctx, tx, f)
	if err != nil {
		return nil, &platform.Error{
			Err: err,
		}
	}

	ps := make([]platform.Permission, 0, len(mappings))
	for _, m := range mappings {
		p, err := m.ToPermissions()
		if err != nil {
			return nil, &platform.Error{
				Err: err,
			}
		}

		ps = append(ps, p...)
	}
	ps = append(ps, platform.MePermissions(s.UserID)...)
	s.Permissions = ps
	return s, nil
}

// PutSession puts the session at key.
func (c *Client) PutSession(ctx context.Context, s *platform.Session) error {
	return c.db.Update(func(tx *bolt.Tx) error {
		if err := c.putSession(ctx, tx, s); err != nil {
			return err
		}
		return nil
	})
}

func (c *Client) putSession(ctx context.Context, tx *bolt.Tx, s *platform.Session) *platform.Error {
	v, err := json.Marshal(s)
	if err != nil {
		return &platform.Error{
			Err: err,
		}
	}
	if err := tx.Bucket(sessionBucket).Put([]byte(s.Key), v); err != nil {
		return &platform.Error{
			Err: err,
		}
	}
	return nil
}

// ExpireSession expires the session at the provided key.
func (c *Client) ExpireSession(ctx context.Context, key string) error {
	return c.db.Update(func(tx *bolt.Tx) error {
		s, err := c.findSession(ctx, tx, key)
		if err != nil {
			return err
		}

		s.ExpiresAt = time.Now()

		if err := c.putSession(ctx, tx, s); err != nil {
			return err
		}
		return nil
	})
}

// CreateSession creates a session for a user with the users maximal privileges.
func (c *Client) CreateSession(ctx context.Context, user string) (*platform.Session, error) {
	var sess *platform.Session
	err := c.db.Update(func(tx *bolt.Tx) error {
		s, err := c.createSession(ctx, tx, user)
		if err != nil {
			return err
		}

		sess = s

		return nil
	})

	if err != nil {
		return nil, err
	}

	return sess, nil
}

func (c *Client) createSession(ctx context.Context, tx *bolt.Tx, user string) (*platform.Session, *platform.Error) {
	u, pe := c.findUserByName(ctx, tx, user)
	if pe != nil {
		return nil, pe
	}

	s := &platform.Session{}
	s.ID = c.IDGenerator.ID()
	k, err := c.TokenGenerator.Token()
	if err != nil {
		return nil, &platform.Error{
			Err: err,
		}
	}
	s.Key = k
	s.UserID = u.ID
	s.CreatedAt = time.Now()
	// TODO(desa): make this configurable
	s.ExpiresAt = s.CreatedAt.Add(time.Hour)
	// TODO(desa): not totally sure what to do here. Possibly we should have a maximal privilege permission.
	s.Permissions = []platform.Permission{}

	if err := c.putSession(ctx, tx, s); err != nil {
		return nil, err
	}

	return s, nil
}
